<?php
/**
 * coreSessionAbstract
 *
 * 세션 핸들러 객체의 추상화 클래스로 일부 인터페이스를 제공하고 있습니다.
 *
 * @package core
 * @author  Platform Team
 */

abstract class coreSessionAbstract
{
    /**
     * 세션 데이터 검증 키
     */
    private $verifyKeyId;
    private $verifyKeyIpAddr;
    private $verifyKeyAccess;
    private $verifyKeyInitAccess;

    /**
     * 아이피 검증 사용 여부
     */
    private $validIpUse;

    /**
     * 아이피 검증 예외처리 핸들러 사용 여부
     */
    private $validIpExceptionUse;

    /**
     * 세션 데이터
     */
    protected $aData;

    /**
     * 현재 타임 스탬프 값
     */
    protected $iNow;

    /**
     * 환경 설정
     */
    protected $sessName;
    protected $sessDomain;
    protected $sessNameByCookie;
    protected $sessType;
    protected $sessMemcacheHost;
    protected $sessMemcachePort;
    protected $sessMemcachePersistent;
    protected $sessExpire;
    protected $sessEncrypt;
    protected $sessEncryptHandler;
    protected $sessEncryptIv;
    protected $sessEncryptKey;
    protected $sessGcProbability;
    protected $sessGcMaxlife;
    protected $sessSavePath;
    protected $sessHashBits;
    protected $sessHashFunction;
    protected $sessCookieLifetime;
    protected $sessCookiePath;

    /**
     * 세션 데이터 저장 억제 여부
     */
    private $isPreventSave;


    /**
     * Construct
     *
     * @param array $aConfig
     */
    final public function __construct($aConfig)
    {
        $this->isPreventSave          = false;
        $this->iNow                   = time();
        $this->sessName               = $aConfig['sess_name'];
        $this->sessNameByCookie       = $aConfig['sess_name_by_cookie'];
        $this->sessDomain             = $this->setSessDomain($aConfig['sess_domain']);
        $this->sessType               = $aConfig['sess_type'];
        $this->sessMemcacheHost       = $aConfig['sess_memcache_host'];
        $this->sessMemcachePort       = $aConfig['sess_memcache_port'];
        $this->sessMemcachePersistent = $aConfig['sess_memcache_persistent'];
        $this->sessExpire             = $aConfig['sess_expire'];
        $this->sessEncrypt            = $aConfig['sess_encrypt'];
        $this->sessEncryptHandler     = $aConfig['sess_encrypt_handler'];
        $this->sessEncryptIv          = $aConfig['sess_encrypt_iv'];
        $this->sessEncryptKey         = $aConfig['sess_encrypt_key'];
        $this->sessGcProbability      = $aConfig['sess_gc_probability'];
        $this->sessGcMaxlife          = $aConfig['sess_gc_maxlife'];
        $this->sessSavePath           = $aConfig['sess_files_save_path'];
        $this->sessHashBits           = $aConfig['sess_hash_bits_per_character'];
        $this->sessHashFunction       = $aConfig['sess_hash_function'];
        $this->sessCookieLifetime     = $aConfig['sess_cookie_lifetime'];
        $this->sessCookiePath         = $aConfig['sess_cookie_path'];

        $this->verifyKeyId            = $aConfig['sess_verify_key_id'];
        $this->verifyKeyIpAddr        = $aConfig['sess_verify_key_ipaddr'];
        $this->verifyKeyAccess        = $aConfig['sess_verify_key_access'];
        $this->verifyKeyInitAccess    = $aConfig['sess_verify_key_init_access'];

        $this->validIpUse             = $aConfig['sess_ip_valid_use'];
        $this->validIpExceptionUse    = $aConfig['sess_ip_valid_exception_use'];

        if ($this->sessExpire == 0) {
            $this->sessExpire = 86400 * 365 * 3;
        }

        $this->initSettingByIni();
        $this->initSessNameByIni();
        $this->initSaveHandlerByIni();
        $this->initialize();

        $this->registSaveHandler();
        $this->startAndDataset();
        $this->individualOpen();

        $this->verify();
    }

    /**
     * INI 설정 - 기본
     */
    protected function initSettingByIni()
    {
        ini_set('session.save_path', $this->sessSavePath);
        ini_set('session.gc_probability', $this->sessGcProbability);
        ini_set('session.gc_maxlifetime', $this->sessGcMaxlife);
        ini_set('session.cookie_domain', $this->sessDomain);
        ini_set('session.hash_bits_per_character', $this->sessHashBits);
        ini_set('session.hash_function', $this->sessHashFunction);
        ini_set('session.cookie_lifetime', $this->sessCookieLifetime);
        ini_set('session.cookie_path', $this->sessCookiePath);

        ini_set('session.auto_start', 0);
        ini_set('session.cookie_httponly', 1);
        ini_set('session.use_cookies', 1);
        ini_set('session.use_trans_sid', 0);
        ini_set('session.gc_divisor', 100);
        ini_set('session.bug_compat_42', 1);
        ini_set('session.bug_compat_warn', 1);
        ini_set('url_rewriter.tags', '');
    }

    /**
     * INI 설정 - 세션네임 (세션 아이디가 쿠키로 구워지는 이름)
     * 드라이버가 쿠키타입인 경우 해당 네이밍으로 세션아이디가 구워지는 문제가 있으므로
     * 해당 부분을 오버라이딩 가능한 메소드로 분리
     */
    protected function initSessNameByIni()
    {
        ini_set('session.name', $this->sessName);
    }

    /**
     * INI 설정 - session.save_handler
     */
    protected function initSaveHandlerByIni()
    {
        ini_set('session.save_handler', 'user');
    }

    /**
     * 드라이버별 Save Handler 등록
     */
    protected function registSaveHandler()
    {
        session_set_save_handler(
            array($this, 'open'),
            array($this, 'close'),
            array($this, 'read'),
            array($this, 'write'),
            array($this, 'destroy'),
            array($this, 'gc')
        );
    }

    /**
     * 세션 스타트 및 세션데이터 초기화 할당
     */
    protected function startAndDataset()
    {
        session_start();
        $this->aData = &$_SESSION;
    }

    /**
     * 초기화 템플릿 메소드
     * (생성자 이후 실행되므로, 사용시에는 오버라이딩 하여 사용합니다)
     */
    protected function initialize(){}

    /**
     * open Handler
     */
    abstract public function open($sSavePath, $sSessName);

    /**
     * close Handler
     */
    abstract public function close();

    /**
     * read Handler
     */
    abstract public function read($sSessId);

    /**
     * write Handler
     */
    abstract public function write($sSessId, $sData);

    /**
     * destroy Handler
     */
    abstract public function destroy($sSessId);

    /**
     * garbage collector Handler
     */
    abstract public function gc($iMaxLife);

    /**
     * 세션 아이디 반환
     *
     * @return string
     */
    public function sessionId()
    {
        return session_id();
    }

    /**
     * 세션 아이디 다시 생성
     *
     * @return boolean
     */
    public function sessionIdRegenerate()
    {
        return session_regenerate_id();
    }

    /**
     * Session Register
     * (두번째 인자를 주지 않으면 unset 으로 동작)
     *
     * @param string $sId
     * @param mixed $mData
     */
    public function set($sId, $mData=null)
    {
        if ($mData === null) {
            unset($this->aData[$sId]);
            return;
        }

        $this->aData[$sId] = $mData;
    }

    /**
     * Session Read
     * (인자가 없을 시 전체 데이터를 반환)
     * (데이터가 없는 경우는 false 반환)
     *
     * @param string $sId
     * @return mixed
     */
    public function get($sId=null)
    {
        if ($sId === null) {
            return $this->aData;
        }

        return isset($this->aData[$sId]) ? $this->aData[$sId] : false;
    }

    /**
     * Session Destroy
     *
     * @return boolean
     */
    public function destruct()
    {
        return session_destroy();
    }

    /**
     * startAndDataset 메소드 실행 이후
     * 사실 이 메소드는 순전히 Cookie 방식의 세션드라이버때문에 생겨남
     * 세션핸들러의 write 메소드 내부에서 header 를 쓸수 없기때문에 코어단 플로우를 활용하는 방안으로 대체
     *
     * 각 드라이버에서는 필요할 경우 오버라이딩 해서 사용하도록 함
     */
    public function individualOpen()
    {
        //Empty
    }

    /**
     * 코어의 autoDestructExecute 함수내부 호출용
     * 사실 이 메소드는 순전히 Cookie 방식의 세션드라이버때문에 생겨남
     * 세션핸들러의 write 메소드 내부에서 header 를 쓸수 없기때문에 코어단 플로우를 활용하는 방안으로 대체
     *
     * 각 드라이버에서는 필요할 경우 오버라이딩 해서 사용하도록 함
     */
    public function individualClose()
    {
        //Empty
    }

    /**
     * 세션 데이터의 저장 억제
     *
     * @param boolean $isEnable
     * @return boolean
     */
    public function preventSave($isEnable=true)
    {
        if ($isEnable !== true) {
            $this->isPreventSave = false;
        } else {
            $this->isPreventSave = true;
        }

        return $this->isPreventSave;
    }

    /**
     * 세션 데이터의 저장 억제 상태 반환
     *
     * @return boolean
     */
    public function getPreventSave()
    {
        return $this->isPreventSave;
    }

    /**
     * 세션데이터에 대한 검증을 시도한다.
     * 검증이 실패할 경우는 aData 멤버 변수가 자동으로 초기화 된다.
     * 검증이 통과하면 access 타임을 자동 업데이트 한다.
     */
    private function verify()
    {
        if (empty($this->aData)) {
            $this->aData = $this->createVerifyData();
            return;
        }

        $sSessId    = $this->aData[$this->verifyKeyId];
        $sIp        = $this->aData[$this->verifyKeyIpAddr];
        $iAccess    = $this->aData[$this->verifyKeyAccess];

        if ($this->verifySessId($sSessId) === false) {
            $this->sessionIdRegenerate();
            $this->aData = $this->createVerifyData();
            return;
        }

        if ($this->verifyExpire($iAccess, $this->sessExpire) === false) {
            $this->sessionIdRegenerate();
            $this->aData = $this->createVerifyData();
            return;
        }

        if ($this->validIpUse === true) {

            if ($this->verifyIp($sIp) === false) {
                if ($this->validIpExceptionUse === true) {
                    $sErrorMsg = 'Session - IP validation failed.';
                    Exceptioner::run(FW_EXCEPTION_SESSION, $sErrorMsg, Session::ERROR_IP_VALID);
                    throw new Exception($sErrorMsg, E_ERROR);
                }

                $this->sessionIdRegenerate();
                $this->aData = $this->createVerifyData();
                return;
            }

        }

        $this->updateVerifyData();
    }

    /**
     * 세션 아이디 검증
     *
     * @param string $sId
     * @return boolean
     */
    private function verifySessId($sId)
    {
        if (empty($sId)) {
            return false;
        }

        if ($sId != $this->sessionId()) {
            return false;
        }

        return true;
    }

    /**
     * 아이피 검증
     *
     * @param string $sIp
     * @return boolean
     */
    private function verifyIp($sIp)
    {
        if (empty($sIp)) {
            return false;
        }

        if ($sIp != $this->realIp()) {
            return false;
        }

        return true;
    }

    /**
     * 지속시간 검증
     *
     * @param int $iAccess
     * @param int $iExpire
     * @return boolean
     */
    private function verifyExpire($iAccess, $iExpire)
    {
        $iExpire = (int)$iExpire;
        $iAccess = (int)$iAccess;

        if ($iExpire === 0) {
            return true;
        }

        $iVerify = $this->iNow - $iExpire;

        if ($iVerify < $iAccess) {
            return true;
        }

        return false;
    }



    /**
     * 검증을 위한 데이터를 반환
     *
     * @return array
     */
    private function createVerifyData()
    {
        $aData = array(
            $this->verifyKeyId => $this->sessionId(),
            $this->verifyKeyIpAddr => $this->realIp(),
            $this->verifyKeyInitAccess => $this->iNow,
            $this->verifyKeyAccess => $this->iNow
        );

        return $aData;
    }

    /**
     * 검증 데이터의 업데이트를 실시
     *
     * @return boolean
     */
    private function updateVerifyData()
    {
        if (empty($this->aData)) {
            return false;
        }

        $this->aData[$this->verifyKeyAccess] = $this->iNow;

        return true;
    }

    /**
     * 아이피 반환
     *
     * @return string
     */
    private function realIp()
    {
        return getClientRealIp();
    }

    /**
     * 세션 유효도메인 설정
     *
     * @param string $sDomain
     * @return string
     */
    private function setSessDomain($sDomain)
    {
        if (!$sDomain) {
            $sDomain = $_SERVER['HTTP_HOST'];
        }

        $sDomain = strtolower($sDomain);

        $sDomain = preg_replace('/^www/', '', $sDomain);

        return $sDomain;
    }

    /**
     * 암호화 핸들러
     * (핸들러 메소드는 encryptHandler[핸들러명] 으로 구현합니다.)
     * 2.0 프레임워크의 호환때문에 추후 구조상 리팩토링이 필요합니다.
     *
     * @param string $sVal
     * @return string
     */
    private function encryptHandler($sVal)
    {
        $sCall = 'encryptHandler' . $this->sessEncryptHandler;

        return call_user_func(array($this, $sCall), $sVal);
    }

    /**
     * 복호화 핸들러
     * (핸들러 메소드는 decryptHandler[핸들러명] 으로 구현합니다.)
     * 2.0 프레임워크의 호환때문에 추후 구조상 리팩토링이 필요합니다.
     *
     * @param string $sVal
     * @return mixed
     */
    private function decryptHandler($sVal)
    {
        $sCall = 'decryptHandler' . $this->sessEncryptHandler;

        return call_user_func(array($this, $sCall), $sVal);
    }

    /**
     * 암호화 핸들러 - 12r
     * @param string $sVal
     */
    private function encryptHandler12r($sVal)
    {
        return utilDes::encrypt($sVal, $this->sessEncryptKey, $this->sessEncryptIv);
    }

    /**
     * 복호화 핸들러 - 12r
     * @param string $sVal
     */
    private function decryptHandler12r($sVal)
    {
        return utilDes::decrypt($sVal, $this->sessEncryptKey, $this->sessEncryptIv);
    }

    /**
     * 암호화 핸들러 - EC 2.0 프레임워크
     * @param string $sVal
     */
    private function encryptHandlerEc20($sVal)
    {
        $sData = http_build_query($sVal, '', '&');

        return rawurlencode(utilDes::encrypt($sData, $this->sessEncryptKey, $this->sessEncryptIv));
    }

    /**
     * 복호화 핸들러 - EC 2.0 프레임워크
     * @param string $sVal
     */
    private function decryptHandlerEc20($sVal)
    {
        $sData = rawurldecode($sVal);

        $str = utilDes::decrypt($sData, $this->sessEncryptKey, $this->sessEncryptIv);
        parse_str($str, $result);
        return $result;
    }

    /**
     * 로드 데이터의 원복
     * (실패 시 false 반환)
     *
     * @param string $sVal
     * @return mixed
     */
    protected function loadData($sVal)
    {
        if (empty($sVal)) {
            return false;
        }

        if ($this->sessEncrypt === false) {
            return $sVal;
        }

        return $this->decryptHandler($sVal);
    }

    /**
     * 저장 데이터의 가공
     * (실패 시 false 반환)
     *
     * @param string $sVal
     * @return mixed
     */
    protected function saveData($sVal)
    {
        if (empty($sVal)) {
            return false;
        }

        if ($this->sessEncrypt === false) {
            return $sVal;
        }

        return $this->encryptHandler($sVal);
    }
}
